package transactor

import (
	reqContext "context"
	"math/rand"
	"sync"
	"time"

	"github.com/golang/protobuf/proto"
	"github.com/golang/protobuf/ptypes"
	"git.cloud.inspur.com/ichain/ichain-protos-go/common"
	pb "git.cloud.inspur.com/ichain/ichain-protos-go/inode"
	"git.cloud.inspur.com/ichain/ichain-sdk-go/pkg/common/errors/multi"
	"git.cloud.inspur.com/ichain/ichain-sdk-go/pkg/common/logging"
	contextImpl "git.cloud.inspur.com/ichain/ichain-sdk-go/pkg/ichain/context"
	ctxprovider "git.cloud.inspur.com/ichain/ichain-sdk-go/pkg/providers/context"
	"git.cloud.inspur.com/ichain/ichain-sdk-go/pkg/providers/ichain"
	"git.cloud.inspur.com/ichain/ichain-sdk-go/thirdparty/ichain/common/crypto"
	"git.cloud.inspur.com/ichain/ichain-sdk-go/thirdparty/ichain/protoutil"
	"github.com/pkg/errors"
)

var logger = logging.NewLogger("ichainsdk/transactor")

// Transactor
// @Description: 负责创建/发送提案 创建/发送交易
type Transactor struct {
	reqCtx  reqContext.Context
	chainID string
}

func NewTransactor(reqCtx reqContext.Context, chainId string) *Transactor {
	return &Transactor{
		reqCtx:  reqCtx,
		chainID: chainId,
	}
}

// NewTxHeader 创建交易头
func (t *Transactor) NewTxHeader() (ichain.TransactionHeader, error) {
	ctx, ok := contextImpl.RequestClientContext(t.reqCtx)
	if !ok {
		return nil, errors.New("failed get client context from reqContext for txn Header")
	}

	// 生成随机数
	nonce, err := crypto.GetRandomNonce()
	if err != nil {
		return nil, err
	}

	// 身份序列化
	creator, err := ctx.Serialize()
	if err != nil {
		return nil, errors.WithMessage(err, "identity from context failed")
	}

	h, err := ctx.CryptoSuite().GetHash()
	if err != nil {
		return nil, errors.WithMessage(err, "not find hash")
	}

	// 生成交易id
	id, err := computeTxnID(nonce, creator, h)
	if err != nil {
		return nil, errors.WithMessage(err, "txn ID computation failed")
	}

	// 构造交易头
	txnID := transactionHeader{
		id:      ichain.TransactionID(id),
		creator: creator,
		nonce:   nonce,
		chainID: t.chainID,
	}

	return &txnID, nil
}

func (t *Transactor) CreateTxProposalWithHeader(header ichain.TransactionHeader, request ichain.ContractInvokeRequest) (*ichain.TransactionProposal, error) {
	if request.ContractID == "" {
		return nil, errors.New("ContractID is required")
	}

	if request.Fcn == "" {
		return nil, errors.New("Fcn is required")
	}

	// 函数名添加到参数内
	argsArray := make([][]byte, len(request.Args)+1)
	argsArray[0] = []byte(request.Fcn)
	for i, arg := range request.Args {
		argsArray[i+1] = arg
	}

	ccis := &pb.ContractInvocationSpec{ContractSpec: &pb.ContractSpec{
		Type: request.Lang, ContractId: &pb.ContractID{Name: request.ContractID},
		Input: &pb.ContractInput{Args: argsArray}}}

	// 创建合约调用提案
	proposal, _, err := protoutil.CreateContractProposalWithTxIDNonceAndTransient(
		string(header.TransactionID()),
		common.HeaderType_ENDORSER_TRANSACTION,
		header.ChainID(), ccis, header.Nonce(), header.Creator(), request.TransientMap)
	if err != nil {
		return nil, errors.Wrap(err, "failed to create contract proposal")
	}

	tp := ichain.TransactionProposal{
		TxnID:    header.TransactionID(),
		Proposal: proposal,
	}

	return &tp, nil
}

// CreateTxProposal
// @Description:  创建交易提案
// @return *ichain.TransactionProposal
// @return error
func (t *Transactor) CreateTxProposal(request ichain.ContractInvokeRequest) (*ichain.TransactionProposal, error) {
	// 构造提案头
	header, err := t.NewTxHeader()
	if err != nil {
		return nil, err
	}

	return t.CreateTxProposalWithHeader(header, request)
}

// SendTxProposal
// @Description: 发送提案
func (t *Transactor) SendTxProposal(proposal *ichain.TransactionProposal, targets []ichain.Inode) ([]*ichain.TransactionProposalResponse, error) {
	if proposal == nil {
		return nil, errors.New("proposal is required")
	}

	targets = getTargetsWithoutDuplicates(targets)

	if len(targets) < 1 {
		return nil, errors.New("targets is required")
	}

	// 获取sdk ctx
	ctx, ok := contextImpl.RequestClientContext(t.reqCtx)
	if !ok {
		return nil, errors.New("failed get client context from reqContext for signProposal")
	}

	// 签名
	signedProposal, err := signProposal(ctx, proposal.Proposal)
	if err != nil {
		return nil, errors.WithMessage(err, "sign proposal failed")
	}

	request := ichain.ProcessProposalRequest{SignedProposal: signedProposal}

	var responseMtx sync.Mutex
	var transactionProposalResponses []*ichain.TransactionProposalResponse
	var wg sync.WaitGroup
	errs := multi.Errors{}

	for _, p := range targets {
		wg.Add(1)
		go func(inode ichain.Inode) {
			defer wg.Done()

			resp, err := inode.ProcessTransactionProposal(t.reqCtx, request)
			if err != nil {
				logger.Debugf("Received error response from txn proposal processing: %s", err)
				responseMtx.Lock()
				errs = append(errs, err)
				responseMtx.Unlock()
				return
			}

			responseMtx.Lock()
			transactionProposalResponses = append(transactionProposalResponses, resp)
			responseMtx.Unlock()
		}(p)
	}
	wg.Wait()

	return transactionProposalResponses, errs.ToError()
}

// NewTx
// @Description: 根据提案生成交易
// @param request
// @return *ichain.Transaction
// @return error
func (t *Transactor) NewTx(request ichain.TransactionRequest) (*ichain.Transaction, error) {
	if len(request.ProposalResponses) == 0 {
		return nil, errors.New("at least one proposal response is necessary")
	}

	proposal := request.Proposal

	// 解析提案头 获取签名头信息 hdr.SignatureHeader
	hdr, err := protoutil.UnmarshalHeader(proposal.Header)
	if err != nil {
		return nil, errors.Wrap(err, "unmarshal proposal header failed")
	}

	// 解析提案负载
	proposalPayload, err := protoutil.UnmarshalContractProposalPayload(proposal.Payload)
	if err != nil {
		return nil, errors.Wrap(err, "unmarshal proposal payload failed")
	}

	// 验证提案
	if vprErr := validateProposalResponses(request.ProposalResponses); vprErr != nil {
		return nil, vprErr
	}

	// 添加背书信息
	endorsements := make([]*pb.Endorsement, len(request.ProposalResponses))
	for n, r := range request.ProposalResponses {
		endorsements[n] = r.ProposalResponse.Endorsement
	}

	responsePayload := request.ProposalResponses[0].ProposalResponse.Payload
	cea := &pb.ContractEndorsedAction{ProposalResponsePayload: responsePayload, Endorsements: endorsements}

	// 重新序列化 去掉TransientMap信息
	propPayloadBytes, err := protoutil.GetBytesProposalPayloadForTx(proposalPayload)
	if err != nil {
		return nil, err
	}

	acPayload := &pb.ContractActionPayload{ContractProposalPayload: propPayloadBytes, Action: cea}
	capBytes, err := protoutil.GetBytesContractActionPayload(acPayload)
	if err != nil {
		return nil, err
	}

	// 提案签名+contract action
	txAction := &pb.TransactionAction{Header: hdr.SignatureHeader, Payload: capBytes}
	txActionLst := []*pb.TransactionAction{txAction}

	return &ichain.Transaction{
		Transaction: &pb.Transaction{Actions: txActionLst},
		Proposal:    proposal,
	}, nil
}

// SendPayload
// @Description: 直接发送负载
func (t *Transactor) SendPayload(payload *common.Payload, consensers []ichain.Consenser) (*common.Block, error) {
	if len(consensers) == 0 {
		return nil, errors.New("consensers not set")
	}
	// 在上下文获取sdk context
	ctx, ok := contextImpl.RequestClientContext(t.reqCtx)
	if !ok {
		return nil, errors.New("failed get client context from reqContext for signPayload")
	}
	envelope, err := signPayload(ctx, payload)
	if err != nil {
		return nil, err
	}

	// 不修改传入的切片
	randConsensers := make([]ichain.Consenser, len(consensers))
	copy(randConsensers, consensers)

	// 随机发送
	var errResp error
	for _, i := range rand.Perm(len(randConsensers)) {
		resp, err := t.sendEnvelope(envelope, randConsensers[i])
		if err != nil {
			errResp = err
		} else {
			return resp, nil
		}
	}
	return nil, errResp
}

// sendEnvelope 发送信封获取生成的区块
func (t *Transactor) sendEnvelope(envelope *ichain.SignedEnvelope, consenser ichain.Consenser) (*common.Block, error) {

	logger.Debugf("Broadcasting envelope to consenser :%s\n", consenser.URL())

	// 共识节点发送
	blocks, errs := consenser.SendDeliver(t.reqCtx, envelope)

	var block *common.Block
	var err multi.Errors
read:
	for {
		// 监听返回的区块
		select {
		case b, ok := <-blocks:
			if !ok {
				break read
			}
			block = b
		case e := <-errs:
			err = append(err, e)
		}
	}

	// 合并error
	for i := 0; i < len(errs); i++ {
		e := <-errs
		err = append(err, e)
	}

	return block, err.ToError()
}

// BroadcastPayload 广播负载
func (t *Transactor) BroadcastPayload(payload *common.Payload, consensers []ichain.Consenser) (*ichain.TransactionResponse, error) {
	if len(consensers) == 0 {
		return nil, errors.New("consensers not set")
	}

	ctx, ok := contextImpl.RequestClientContext(t.reqCtx)
	if !ok {
		return nil, errors.New("failed get client context from reqContext for signPayload")
	}
	envelope, err := signPayload(ctx, payload)
	if err != nil {
		return nil, err
	}

	return t.broadcastEnvelope(envelope, consensers)
}

func (t *Transactor) broadcastEnvelope(envelope *ichain.SignedEnvelope, consensers []ichain.Consenser) (*ichain.TransactionResponse, error) {
	if len(consensers) == 0 {
		return nil, errors.New("consensers not set")
	}

	ctxClient, ok := contextImpl.RequestClientContext(t.reqCtx)
	if !ok {
		return nil, errors.New("failed get client context from reqContext for SendTransaction")
	}

	randConsensers := make([]ichain.Consenser, len(consensers))
	copy(randConsensers, consensers)

	// 随机一个去广播
	var errResp error
	for _, i := range rand.Perm(len(randConsensers)) {
		resp, err := t.sendBroadcast(envelope, randConsensers[i], ctxClient)
		if err != nil {
			errResp = err
		} else {
			return resp, nil
		}
	}
	return nil, errResp
}

func (t *Transactor) sendBroadcast(envelope *ichain.SignedEnvelope, consenser ichain.Consenser, client ctxprovider.Client) (*ichain.TransactionResponse, error) {
	logger.Debugf("Broadcasting envelope to consenser: %s\n", consenser.URL())

	childCtx, cancel := contextImpl.NewRequest(client, contextImpl.WithTimeoutType(ichain.ConsenserResponse), contextImpl.WithParent(t.reqCtx))
	defer cancel()

	if _, err := consenser.SendBroadcast(childCtx, envelope); err != nil {
		logger.Debugf("Receive Error Response from consenser: %s\n", err)
		return nil, errors.Wrapf(err, "calling consenser '%s' failed", consenser.URL())
	}

	logger.Debugf("Receive Success Response from consenser\n")
	return &ichain.TransactionResponse{Consenser: consenser.URL()}, nil
}

func (t *Transactor) SendTx(tx *ichain.Transaction) (*ichain.TransactionResponse, error) {
	if tx == nil {
		return nil, errors.New("transaction is nil")
	}
	if tx.Proposal == nil || tx.Proposal.Proposal == nil {
		return nil, errors.New("proposal is nil")
	}

	ctx, ok := contextImpl.RequestClientContext(t.reqCtx)
	if !ok {
		return nil, errors.New("not find sdk ctx from req ctx")
	}

	consensers, err := consensersFromChain(ctx, t.chainID)
	if err != nil {
		return nil, errors.WithMessage(err, "get chain consenser from config fail")
	}

	// pb header
	hdr, err := protoutil.UnmarshalHeader(tx.Proposal.Proposal.Header)
	if err != nil {
		return nil, errors.Wrap(err, "unmarshal proposal header failed")
	}
	txBytes, err := protoutil.GetBytesTransaction(tx.Transaction)
	if err != nil {
		return nil, err
	}

	payload := common.Payload{Header: hdr, Data: txBytes}

	txResponse, err := t.BroadcastPayload(&payload, consensers)
	if err != nil {
		return nil, err
	}

	return txResponse, nil
}

// CreatePayload
// @Description: 创建负载
// @return *common.Payload
// @return error
func (t *Transactor) CreatePayload(headerType common.HeaderType, opts ichain.ChainHeaderOpts, msg proto.Message) (*common.Payload, error) {
	if msg == nil {
		return nil, errors.New("msg is nil")
	}
	header, err := t.createHeader(headerType, opts)
	if err != nil {
		return nil, errors.WithMessage(err, "create header fail")
	}
	data, err := proto.Marshal(msg)
	if err != nil {
		return nil, errors.WithMessage(err, "marshal proto message fail")
	}
	return &common.Payload{Header: header, Data: data}, nil
}

func (t *Transactor) createHeader(headerType common.HeaderType, opts ichain.ChainHeaderOpts) (*common.Header, error) {
	// 创建chain header
	if opts.TxnHeader == nil {
		th, err := t.NewTxHeader()
		if err != nil {
			return nil, errors.WithMessage(err, "new tx header fail")
		}
		opts.TxnHeader = th
	}

	chainHeader, err := t.createChainHeader(headerType, opts.TxnHeader.ChainID(), string(opts.TxnHeader.TransactionID()), opts)
	if err != nil {
		return nil, errors.WithMessage(err, "create chain header fail")
	}
	// 构造签名头
	signatureHeader := &common.SignatureHeader{
		Creator: opts.TxnHeader.Creator(),
		Nonce:   opts.TxnHeader.Nonce(),
	}
	sh, err := proto.Marshal(signatureHeader)
	if err != nil {
		return nil, errors.Wrap(err, "marshal signatureHeader failed")
	}
	ch, err := proto.Marshal(chainHeader)
	if err != nil {
		return nil, errors.Wrap(err, "marshal chainHeader failed")
	}

	// 组装 header
	header := common.Header{
		SignatureHeader: sh,
		ChainHeader:     ch,
	}
	return &header, nil
}

func (t *Transactor) createChainHeader(headerType common.HeaderType, chainId, txId string, opts ichain.ChainHeaderOpts) (*common.ChainHeader, error) {
	logger.Debugf("buildChainHeader - headerType: %s chainID: %s txID: %d epoch: %d contractID: %s timestamp: %v",
		headerType, chainId, txId, opts.Epoch, opts.ContractID, opts.Timestamp)
	chainHeader := &common.ChainHeader{
		Type:        int32(headerType),
		ChainId:     chainId,
		TxId:        txId,
		Epoch:       opts.Epoch,
		TlsCertHash: opts.TLSCertHash,
	}

	if opts.Timestamp.IsZero() {
		opts.Timestamp = time.Now()
	}

	ts, err := ptypes.TimestampProto(opts.Timestamp)
	if err != nil {
		return nil, errors.Wrap(err, "failed to create timestamp in chain header")
	}
	chainHeader.Timestamp = ts

	if opts.ContractID != "" {
		ccID := &pb.ContractID{
			Name: opts.ContractID,
		}
		headerExt := &pb.ContractHeaderExtension{
			ContractId: ccID,
		}
		headerExtBytes, err := proto.Marshal(headerExt)
		if err != nil {
			return nil, errors.Wrap(err, "marshal header extension failed")
		}
		chainHeader.Extension = headerExtBytes
	}
	return chainHeader, nil
}
